﻿Public Class TreeView : Inherits UserControl

    Public Highlight As Color = Color.FromArgb(255, 192, 0)

    Dim WithEvents Timer As New Timer With {.Interval = 500}
    Public WithEvents EditBox As New TextBox
    Friend MustMeasure As Boolean

    Sub New()
        InitializeComponent()
        Controls.Add(EditBox)
        EditBox.Visible = False
        EditBox.AutoSize = False
        EditBox.BackColor = Color.White
        EditBox.ForeColor = Color.DarkRed
        EditBox.BorderStyle = BorderStyle.None
        EditBox.Font = New Font("segoe ui", 9)
        EditBox.Multiline = True
        DoubleBuffered = True
        ResizeRedraw = True
        AllowDrop = True
    End Sub

    Dim _Indentation As Integer = 13
    Property Indentation As Integer
        Get
            Return _Indentation
        End Get
        Set(ByVal Value As Integer)
            If Value < 8 Then Exit Property
            If Value > 64 Then Exit Property
            _Indentation = Value
            MustMeasure = True
            Invalidate()
        End Set
    End Property

    Property AutoImage As Boolean
    Property ImageLeaf As Image
    Property ImageFolder As Image

    Protected Overrides Function IsInputKey(ByVal keyData As System.Windows.Forms.Keys) As Boolean
        Select Case keyData
            Case Keys.Left, Keys.Right, Keys.Up, Keys.Down : Return True
        End Select
        Return MyBase.IsInputKey(keyData)
    End Function

    Event AfterLabelEdit(ByVal sender As Object, ByVal e As System.Windows.Forms.NodeLabelEditEventArgs)

    Property FullRowSelect As Boolean

    Property HideSelection As Boolean

    Property LabelEdit As Boolean

    Dim _ShowLines As Boolean
    Property ShowLines As Boolean
        Get
            Return _ShowLines
        End Get
        Set(ByVal Value As Boolean)
            _ShowLines = Value
            Invalidate()
        End Set
    End Property

    Friend WithEvents ScrollBar As System.Windows.Forms.VScrollBar

    'Public Root As New TreeNode(Me) With {.Root = True, .Expanded = True}
    Public Nodes As New TreeNodes(Me, Nothing)

    Dim m_SelectedNode As TreeNode
    Property SelectedNode As TreeNode
        Get
            Return m_SelectedNode
        End Get
        Set(ByVal Value As TreeNode)
            If m_SelectedNode Is Value Then Exit Property
            Timer.Stop()
            m_SelectedNode = Value
            m_SelectedNode.EnsureVisible()
            Invalidate()
        End Set
    End Property

    Dim _DropNode As TreeNode
    Friend Property DropNode As TreeNode
        Get
            Return _DropNode
        End Get
        Set(ByVal Value As TreeNode)
            If Value Is _DropNode Then Exit Property
            _DropNode = Value
            If Not DropNode Is Nothing Then DropNode.EnsureVisible()
            Invalidate()
        End Set
    End Property

    Dim _DropRect As Rectangle
    Friend Property DropRect As Rectangle
        Get
            Return _DropRect
        End Get
        Set(ByVal Value As Rectangle)
            If Value = _DropRect Then Exit Property
            _DropRect = Value
            If Not DropRect = Nothing Then DropNode.EnsureVisible()
            Invalidate()
        End Set
    End Property

    Dim _Size As Size
    Property ImageSize As Size
        Get
            Return _Size
        End Get
        Set(ByVal Value As Size)
            _Size = Value
            Invalidate()
        End Set
    End Property

    Protected Overrides Sub OnLostFocus(ByVal e As System.EventArgs)
        Invalidate()
        MyBase.OnLostFocus(e)
    End Sub

    Protected Overrides Sub OnGotFocus(ByVal e As System.EventArgs)
        Invalidate()
        MyBase.OnGotFocus(e)
    End Sub

    Sub EndUpdate()
    End Sub

    Sub ExpandAll()
        Nodes.ExpandAll()
    End Sub

    Sub BeginUpdate()
    End Sub

    Sub CollapseAll()
        Nodes.CollapseAll()
    End Sub

    Function Add(ByVal Parent As TreeNode, ByVal Text As String) As TreeNode
        Dim Node As New TreeNode(Me, Parent) With {.Text = Text}
        If SelectedNode Is Nothing Then SelectedNode = Node
        Invalidate()
        Return Node
    End Function

    Function Add(ByVal Parent As TreeNode, ByVal Text As String, ByVal Image As Image) As TreeNode
        Dim Node As New TreeNode(Me, Parent) With {.Text = Text, .Image = Image}
        If SelectedNode Is Nothing Then SelectedNode = Node
        Invalidate()
        Return Node
    End Function

    Function InsertBefore(ByVal RefNode As TreeNode, ByVal Text As String) As TreeNode
        If SelectedNode Is Nothing Then Return Add(Nothing, Text)
        Dim Node As New TreeNode(Me, RefNode.Parent, RefNode.Owner.IndexOf(RefNode))
        Node.Text = Text
        Return Node
    End Function

    Function InsertAfter(ByVal RefNode As TreeNode, ByVal Text As String) As TreeNode
        If SelectedNode Is Nothing Then Return Add(Nothing, Text)
        Dim Node As New TreeNode(Me, RefNode.Parent, RefNode.Owner.IndexOf(RefNode) + 1)
        Node.Text = Text
        Return Node
    End Function

    Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
        Dim y As Integer = 0
        Nodes.Measure(e.Graphics, 0, y, Width - ScrollBar.Width)
        e.Graphics.Clear(Color.White)
        e.Graphics.ResetTransform()
        e.Graphics.TranslateTransform(0, -ScrollBar.Value)
        Dim R = ClientRectangle
        R.Offset(0, ScrollBar.Value)
        If ShowLines Then Nodes.DrawLines(e.Graphics, R)
        Nodes.Draw(e.Graphics, R)
        If y > Height Then
            ScrollBar.Maximum = y
            ScrollBar.SmallChange = 8
            ScrollBar.LargeChange = Height
            ScrollBar.Enabled = True
            If y - ScrollBar.Value < Height Then
                ScrollBar.Value = y - Height
            End If
        Else
            ScrollBar.Value = 0
            ScrollBar.Enabled = False
        End If
        MustMeasure = False
    End Sub

    Private Sub EditBox_Finished(ByVal sender As Object, ByVal Cancelled As Boolean) Handles EditBox.Finished
        If Not Cancelled Then
            If Not RaiseAfterLabelEdit(EditBox.Text) Then SelectedNode.Text = EditBox.Text
        End If
        SelectedNode.EndEdit()
    End Sub

    Dim SameNode As Boolean

    Protected Overrides Sub OnDragDrop(ByVal e As System.Windows.Forms.DragEventArgs)
        MyBase.OnDragDrop(e)
        If e.Data.GetDataPresent(GetType(TreeNode)) Then
            Dim Node As TreeNode = e.Data.GetData(GetType(TreeNode))
            DropNode.Dropped(Node)
        ElseIf e.Data.GetDataPresent(DataFormats.FileDrop) Then
            DropNode.Expanded = True
            For Each Item As String In e.Data.GetData(DataFormats.FileDrop)
                Dim n = Add(DropNode, My.Computer.FileSystem.GetName(Item))
                If My.Computer.FileSystem.DirectoryExists(Item) Then
                    AddFolder(n, Item)
                End If
            Next
        End If
        DropNode = Nothing
    End Sub

    Protected Overrides Sub OnDragLeave(ByVal e As System.EventArgs)
        MyBase.OnDragLeave(e)
        DropNode = Nothing
    End Sub

    Protected Overrides Sub OnDragOver(ByVal e As System.Windows.Forms.DragEventArgs)
        MyBase.OnDragOver(e)
        Dim Point = PointToClient(New Point(e.X, e.Y))
        If Point.Y < 5 Then AttemptScroll(-1)
        If Point.Y > Height - 5 Then AttemptScroll(+1)
        Dim Info = HitTest(Point)
        If Info Is Nothing OrElse Info.Node.Contains(SelectedNode) Then
            DropNode = Nothing
            DropRect = Nothing
            e.Effect = DragDropEffects.None
            Exit Sub
        End If
        If e.Data.GetDataPresent(GetType(TreeNode)) Then
            DropNode = Info.Node
            DropRect = Info.Rectangle
            e.Effect = DragDropEffects.Move
        ElseIf e.Data.GetDataPresent(DataFormats.FileDrop) Then
            DropNode = Info.Node
            DropRect = Info.Rectangle
            e.Effect = DragDropEffects.Move
        End If
    End Sub

    Function HitTest(ByVal Location As Point) As HitTestInfo
        Dim P = Location
        P.Offset(0, ScrollBar.Value)
        Return Nodes.HitTest(P)
    End Function

    Dim ClickPoint As Point

    Protected Overrides Sub OnFontChanged(ByVal e As System.EventArgs)
        MyBase.OnFontChanged(e)
        MustMeasure = True
        Invalidate()
    End Sub

    Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
        MyBase.OnMouseDown(e)
        If SelectedNode Is Nothing Then Exit Sub
        If SelectedNode.IsEditing Then
            SelectedNode.EndEdit()
        End If
        ClickPoint = e.Location
        Dim P = e.Location
        P.Offset(0, ScrollBar.Value)
        SameNode = SelectedNode.TextRect.Contains(P)
        If SameNode Then Exit Sub
        Nodes.ProcessMouse(P)
    End Sub

    Protected Overrides Function ProcessCmdKey(ByRef msg As System.Windows.Forms.Message, ByVal keyData As System.Windows.Forms.Keys) As Boolean
        If Focused Then
            If keyData = Keys.Down Then
                SelectedNode.SelectNext()
                Return True
            ElseIf keyData = Keys.Up Then
                SelectedNode.SelectPrev()
                Return True
            ElseIf keyData = Keys.Right Then
                If Not SelectedNode.Expanded Then
                    SelectedNode.Expanded = True
                    Invalidate()
                    Return True
                End If
            ElseIf keyData = Keys.Left Then
                If SelectedNode.Expanded And SelectedNode.Nodes.Count > 0 Then
                    SelectedNode.Expanded = False
                    Invalidate()
                    Return True
                ElseIf Not SelectedNode.Parent Is Nothing Then
                    SelectedNode = SelectedNode.Parent
                    Return True
                End If
            ElseIf keyData = Keys.Home Then
                Nodes(0).Select()
            ElseIf keyData = Keys.End Then
                Dim Node = Nodes.Last
                Do Until Not Node.Expanded OrElse Node.Nodes.Count = 0
                    Node = Node.Nodes.Last
                Loop
                Node.Select()
            ElseIf keyData = Keys.PageDown Then
                AttemptScroll(Height)
            ElseIf keyData = Keys.PageUp Then
                AttemptScroll(-Height)
            End If
        End If
        Return MyBase.ProcessCmdKey(msg, keyData)
    End Function

    Private Sub InitializeComponent()
        Me.ScrollBar = New System.Windows.Forms.VScrollBar()
        Me.SuspendLayout()
        '
        'ScrollBar
        '
        Me.ScrollBar.Dock = System.Windows.Forms.DockStyle.Right
        Me.ScrollBar.Enabled = False
        Me.ScrollBar.Location = New System.Drawing.Point(283, 0)
        Me.ScrollBar.Name = "ScrollBar"
        Me.ScrollBar.Size = New System.Drawing.Size(17, 248)
        Me.ScrollBar.TabIndex = 0
        '
        'TreeView
        '
        Me.Controls.Add(Me.ScrollBar)
        Me.Name = "TreeView"
        Me.Size = New System.Drawing.Size(300, 248)
        Me.ResumeLayout(False)

    End Sub

    Private Sub ScrollBar_ValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ScrollBar.ValueChanged
        Invalidate()
    End Sub

    Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs)
        MyBase.OnMouseMove(e)
        If SelectedNode Is Nothing Then Exit Sub
        If e.Button = Windows.Forms.MouseButtons.Left Then
            If Math.Abs(e.Location.X - ClickPoint.X) + Math.Abs(e.Location.Y - ClickPoint.Y) > 10 Then
                DoDragDrop(SelectedNode, DragDropEffects.All)
            End If
        End If
    End Sub

    Protected Overrides Sub OnMouseUp(ByVal e As System.Windows.Forms.MouseEventArgs)
        MyBase.OnMouseUp(e)
        If SameNode Then
            Dim P = e.Location
            P.Offset(0, ScrollBar.Value)
            If SelectedNode.TextRect.Contains(P) Then
                Timer.Enabled = True
            End If
        End If
    End Sub

    Protected Overrides Sub OnMouseWheel(ByVal e As System.Windows.Forms.MouseEventArgs)
        MyBase.OnMouseWheel(e)
        AttemptScroll(-e.Delta)
    End Sub

    Private Sub Timer_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles Timer.Tick
        Timer.Stop()
        SelectedNode.BeginEdit()
    End Sub

    Function RaiseAfterLabelEdit(ByVal Text As String) As Boolean
        Dim e As New NodeLabelEditEventArgs(Nothing, Text)
        RaiseEvent AfterLabelEdit(Me, e)
        Return e.CancelEdit
    End Function

    Class TextBox : Inherits System.Windows.Forms.TextBox

        Event Finished(ByVal sender As Object, ByVal Cancelled As Boolean)

        Property MaxWidth As Integer

        Property OriginalText As String

        Protected Overrides Function ProcessCmdKey(ByRef msg As System.Windows.Forms.Message, ByVal keyData As System.Windows.Forms.Keys) As Boolean
            If keyData = Keys.Escape Then
                Text = OriginalText
                Hide()
                Return True
            ElseIf keyData = Keys.Enter Then
                Hide()
                Return True
            End If
            Return MyBase.ProcessCmdKey(msg, keyData)
        End Function

        Private Sub TextBox_LostFocus(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.LostFocus
            Hide()
        End Sub

        Protected Overrides Sub OnTextChanged(ByVal e As System.EventArgs)
            MyBase.OnTextChanged(e)
            Dim Size = TextRenderer.MeasureText(If(Text = "", " ", Text), Font, New Size(MaxWidth, 1), TextFormatFlags.WordBreak Or TextFormatFlags.TextBoxControl)
            Width = Math.Max(Size.Width + 6, 150)
            Height = Size.Height + 5
        End Sub

        Private Sub TextBox_VisibleChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.VisibleChanged
            If Visible Then Exit Sub
            If Text = OriginalText Then Exit Sub
            RaiseEvent Finished(Me, False)
        End Sub

    End Class

    Private Sub EditBox_VisibleChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles EditBox.VisibleChanged
        If Not EditBox.Visible Then Focus()
    End Sub

    Sub Cut()
        If EditBox.Visible Then EditBox.Cut()
    End Sub

    Sub Copy()
        If EditBox.Visible Then EditBox.Copy()
    End Sub

    Sub Paste()
        If EditBox.Visible Then EditBox.Paste()
    End Sub

    Sub SelectAll()
        If EditBox.Visible Then EditBox.SelectAll()
    End Sub

    Protected Overrides Sub OnSizeChanged(ByVal e As System.EventArgs)
        MyBase.OnSizeChanged(e)
        MustMeasure = True
    End Sub

    Private Sub AddFolder(ByVal Parent As TreeNode, ByVal Folder As String)
        For Each Item As String In My.Computer.FileSystem.GetDirectories(Folder)
            Dim n = Add(Parent, My.Computer.FileSystem.GetName(Item))
            AddFolder(n, Item)
        Next
        For Each Item As String In My.Computer.FileSystem.GetFiles(Folder)
            Add(Parent, My.Computer.FileSystem.GetName(Item))
        Next
    End Sub

    Private Sub AttemptScroll(ByVal Amount As Integer)
        If Amount < 0 Then
            ScrollBar.Value = Math.Max(ScrollBar.Value + Amount, 0)
        Else
            ScrollBar.Value = Math.Min(ScrollBar.Value + Amount, ScrollBar.Maximum - ScrollBar.LargeChange)
        End If
    End Sub

    Public Class HitTestInfo

        Public Node As TreeNode
        Public Rectangle As Rectangle

    End Class

End Class

